package jmine.tec.persist.schema.impl.elements;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import jmine.tec.persist.annotation.Constraint;
import jmine.tec.persist.annotation.Constraints;
import jmine.tec.persist.annotation.DiscriminatorComment;
import jmine.tec.persist.schema.api.AbstractProcessingSchemaElement;
import jmine.tec.persist.schema.api.ProcessingSchemaElement;
import jmine.tec.persist.schema.api.SqlDialect;
import jmine.tec.persist.schema.impl.elements.type.AllowedValue;
import jmine.tec.persist.schema.impl.elements.type.ApacheIntEnumTypeStrategy;
import jmine.tec.persist.schema.impl.elements.type.ApacheStringEnumTypeStrategy;
import jmine.tec.persist.schema.impl.elements.type.BlobTypeStrategy;
import jmine.tec.persist.schema.impl.elements.type.BooleanTypeStrategy;
import jmine.tec.persist.schema.impl.elements.type.ClobTypeStrategy;
import jmine.tec.persist.schema.impl.elements.type.DateTypeStrategy;
import jmine.tec.persist.schema.impl.elements.type.EmptyTypeStrategy;
import jmine.tec.persist.schema.impl.elements.type.EnumTypeStrategy;
import jmine.tec.persist.schema.impl.elements.type.IntegerAllowedValue;
import jmine.tec.persist.schema.impl.elements.type.StaticEnumTypeStrategy;
import jmine.tec.persist.schema.impl.elements.type.StringAllowedValue;
import jmine.tec.persist.schema.impl.elements.type.TypeStrategy;

import org.apache.commons.lang.StringUtils;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.type.IntegerType;
import org.hibernate.type.Type;

/**
 * Elemento representando o tipo de uma coluna
 * 
 * @author lundberg
 */
public class ColumnTypeElement extends AbstractProcessingSchemaElement<ColumnProcessingBean> {

    private static final int DEFAULT_COLUMN_LENGTH = 255;

    private int code;

    private int length;

    private int precision;

    private int scale;

    private ColumnElement column;

    private SortedSet<AllowedValue> allowedValues = new TreeSet<AllowedValue>();

    private List<Constraint> constraints = new LinkedList<Constraint>();

    private boolean disableGuesses = false;

    private boolean isDiscriminator = false;

    private List<TypeStrategy> typeStrategies;

    /**
     * Construtor
     * 
     * @param parent mensagenss
     */
    public ColumnTypeElement(ProcessingSchemaElement<?> parent) {
        super(parent);
        this.typeStrategies = new ArrayList<TypeStrategy>();
        this.typeStrategies.add(new StaticEnumTypeStrategy());
        this.typeStrategies.add(new EnumTypeStrategy());
        this.typeStrategies.add(new ApacheStringEnumTypeStrategy());
        this.typeStrategies.add(new ApacheIntEnumTypeStrategy());
        this.typeStrategies.add(new DateTypeStrategy());
        this.typeStrategies.add(new BooleanTypeStrategy());
        this.typeStrategies.add(new ClobTypeStrategy());
        this.typeStrategies.add(new BlobTypeStrategy());
        this.typeStrategies.add(new EmptyTypeStrategy());
    }

    /**
     * {@inheritDoc}
     */
    public List<String> createStatements(SqlDialect dialect) {
        return Collections.singletonList(dialect.renderType(this.code, this.length, this.precision, this.scale));
    }

    /**
     * {@inheritDoc}
     */
    public void process(ColumnProcessingBean metadata) {
        Column columnMapping = metadata.getColumn();
        Type type = columnMapping.getValue().getType();
        Class<?> returnedClass = type.getReturnedClass();
        Method m = metadata.getGetter();
        javax.persistence.Column columnAnnotation = m.getAnnotation(javax.persistence.Column.class);
        javax.persistence.JoinColumn joinColumnAnnotation = m.getAnnotation(javax.persistence.JoinColumn.class);
        TypeStrategy strategy = this.findTypeStrategy(type);
        this.code = strategy.correctSqlType(type, this.code);

        if (columnAnnotation != null
                && (columnAnnotation.length() != DEFAULT_COLUMN_LENGTH || columnAnnotation.precision() != 0 || columnAnnotation.scale() != 0)) {
            this.length = columnAnnotation.length();
            this.precision = columnAnnotation.precision();
            this.scale = columnAnnotation.scale();
            for (String warning : strategy.checkUserDefinedValues(this, returnedClass)) {
                Schema.LOG.warn(warning);
            }
        } else {
            strategy.inferTypeParameters(this, returnedClass);
        }
        if (strategy.hasLimitedAllowedValues()) {
            this.allowedValues.addAll(strategy.getAllowedValues(returnedClass));
        } else if ((joinColumnAnnotation != null && (joinColumnAnnotation.insertable() || joinColumnAnnotation.updatable()))
                || (columnAnnotation != null && (columnAnnotation.insertable() || columnAnnotation.updatable()))) {
            this.disableGuesses = true;
        }
        this.constraints.addAll(this.findConstraints(m));
    }

    /**
     * Encontra as anotações de Contraint contidas no elemento anotado
     * 
     * @param annotated elemento anotado
     * @return lista de Constraints contidas no elemento
     */
    private List<Constraint> findConstraints(AnnotatedElement annotated) {
        List<Constraint> ann = new LinkedList<Constraint>();
        if (annotated.isAnnotationPresent(Constraint.class)) {
            ann.add(annotated.getAnnotation(Constraint.class));
        }
        if (annotated.isAnnotationPresent(Constraints.class)) {
            ann.addAll(Arrays.asList(annotated.getAnnotation(Constraints.class).value()));
        }
        return ann;
    }

    /**
     * Encontra a estratégia apropriada para tratar o tipo da coluna, de acordo com a classe da entidade retornada pela propriedade.
     * 
     * @param type type
     * @return TypeStrategy
     */
    private TypeStrategy findTypeStrategy(Type type) {
        for (TypeStrategy strategy : this.typeStrategies) {
            if (strategy.matches(this, type)) {
                return strategy;
            }
        }
        throw new IllegalStateException();
    }

    /**
     * Processamento específico para o caso da coluna ser referente a um discriminator
     * 
     * @param persistentClass persistentClass
     */
    public void processDiscriminator(PersistentClass persistentClass) {
        this.isDiscriminator = true;
        if (!persistentClass.isInherited()) {
            this.constraints.addAll(this.findConstraints(persistentClass.getMappedClass()));
        }
        if (persistentClass.getDiscriminatorValue() != null && !persistentClass.isAbstract()) {
            String discriminatorDocumentation = null;
            Class<?> mappedClass = persistentClass.getMappedClass();
            if (mappedClass.isAnnotationPresent(DiscriminatorComment.class)) {
                discriminatorDocumentation = mappedClass.getAnnotation(DiscriminatorComment.class).value();
            } else {
                this.warn("Não encontrado @DiscriminatorComment em " + mappedClass.getCanonicalName());
            }
            if (persistentClass.getDiscriminator().getType().getClass().equals(IntegerType.class)) {
                this.allowedValues.add(new IntegerAllowedValue(Integer.parseInt(persistentClass.getDiscriminatorValue()),
                        discriminatorDocumentation));
            } else {
                this.allowedValues.add(new StringAllowedValue(persistentClass.getDiscriminatorValue(), discriminatorDocumentation));
            }
        }
    }

    /**
     * Cria as check constraints definidas para a coluna, criando automaticamente constraints e condições de acordo com o tipo, se possível.
     * 
     * @return conjunto de check constraints que deve ser aplicado
     * @param tableElement tabela
     */
    public Set<CheckConstraintElement> createCheckConstraints(TableElement tableElement) {
        if (!this.constraints.isEmpty()) {
            Set<String> names = new HashSet<String>();
            Set<CheckConstraintElement> checks = new HashSet<CheckConstraintElement>();
            for (Constraint constraint : this.constraints) {
                if (constraint.enabled()) {
                    String constraintName = this.createCheckConstraintName(tableElement, constraint);
                    if (!names.contains(constraintName)) {
                        names.add(constraintName);
                        if (constraint.condition().length() > 1) {
                            checks.add(new CheckConstraintElement(tableElement.getName(), constraintName, constraint.condition(),
                                    this.column));
                        } else if (this.existsAvailableGuess()) {
                            String condition = this.createGuessedCondition();
                            if (!this.allowedValues.isEmpty()) {
                                checks.add(new CheckConstraintElement(tableElement.getName(), constraintName, condition, this.column));
                            }
                        } else {
                            this.warn(String.format("Constraint definido sem condição e não é possível inferir uma para coluna %s",
                                    this.column.toString()));
                        }
                    }
                }
            }
            return checks;
        } else if (this.existsAvailableGuess()) {
            return Collections.singleton(new CheckConstraintElement(tableElement.getName(), null, this.createGuessedCondition(),
                    this.column));
        } else {
            return Collections.emptySet();
        }
    }

    /**
     * Cria o nome do check constraint
     * 
     * @param table table
     * @param constraint constraint
     * @return nome
     */
    @SuppressWarnings("deprecation")
    private String createCheckConstraintName(TableElement table, Constraint constraint) {
        if (!StringUtils.isBlank(constraint.name())) {
            return constraint.name();
        } else if (!StringUtils.isBlank(constraint.value())) {
            return constraint.value();
        } else if (!StringUtils.isBlank(constraint.suffix())) {
            return "CK_" + table.getAlias() + "_" + constraint.suffix();
        } else {
            return "";
        }
    }

    /**
     * Cria uma condição inferida a partir do processamento das ocorrências de mapeamento desta coluna.
     * 
     * @return condição
     */
    private String createGuessedCondition() {
        if (!this.existsAvailableGuess()) {
            throw new IllegalStateException();
        } else {
            List<String> allowed = new LinkedList<String>();
            String template = this.code == Types.NUMERIC ? "%s" : "'%s'";
            for (AllowedValue value : this.allowedValues) {
                allowed.add(String.format(template, value.getValue()));
            }
            this.disableGuesses = true;
            return String.format("%s IN (%s)", this.column.getName(), StringUtils.join(allowed, ", "));
        }
    }

    /**
     * Verifica se existe condição a ser "adivinhada".
     * 
     * @return boolean
     */
    private boolean existsAvailableGuess() {
        return !this.disableGuesses && (this.isDiscriminator || !this.allowedValues.isEmpty());
    }

    /**
     * @param code the code to set
     */
    public void setCode(int code) {
        this.code = code;
    }

    /**
     * @param length the length to set
     */
    public void setLength(int length) {
        this.length = length;
    }

    /**
     * @param precision the precision to set
     */
    public void setPrecision(int precision) {
        this.precision = precision;
    }

    /**
     * @param scale the scale to set
     */
    public void setScale(int scale) {
        this.scale = scale;
    }

    /**
     * @param column the column to set
     */
    public void setColumn(ColumnElement column) {
        this.column = column;
    }

    /**
     * obtém o parâmetro length do tipo
     * 
     * @return int
     */
    public int getLength() {
        return this.length;
    }

    /**
     * obtém o parâmetro precision do tipo
     * 
     * @return int
     */
    public int getPrecision() {
        return this.precision;
    }

    /**
     * obtém o parâmetro scale do tipo
     * 
     * @return int
     */
    public int getScale() {
        return this.scale;
    }

    /**
     * @return the code
     */
    public int getCode() {
        return this.code;
    }

    /**
     * Obtém a coluna que possui o tipo
     * 
     * @return column
     */
    public ColumnElement getColumn() {
        return this.column;
    }

    /**
     * Obtém o conjunto ordenado de valores permitidos
     * 
     * @return conjunto de valores permitidos
     */
    public SortedSet<AllowedValue> getAllowedValues() {
        return this.allowedValues;
    }

}
